package com.seednia.foundation.framework.service;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.Date;
import java.util.List;
import java.util.UUID;

import org.apache.commons.beanutils.PropertyUtils;
import org.springframework.transaction.annotation.Transactional;

import com.seednia.foundation.framework.dto.AbstractRecord;
import com.seednia.foundation.framework.dto.ICriteria;
import com.seednia.foundation.framework.dto.QueryParams;
import com.seednia.foundation.framework.util.RecordUtils;

/**
 * This abstract class define common methods that all service should have
 * 
 * @author Young-Suk
 * @version $Revision: 1.0 $
 */
public abstract class AbstractDataOrientedService extends AbstractService implements IDataOrientedService {

	/**
	 * A class information of data type stored in this model
	 */
	protected Class<? extends AbstractRecord> recordClass = null;
	
	
	protected Class<?> criteriaClass = null;

	/**
	 * The constructor of the class.
	 * It sets the record class.
	 * @param recordClass the Data transfer object class that represents the
	 * 			tuple of the table that this data oriented service handles.
	 */
	public AbstractDataOrientedService(Class<? extends AbstractRecord> recordClass) {
		// modelChangeListenerAdapter = new ModelChangeListenerAdapter(this);
		setRecordClass(recordClass);
	}

	/**
	 * Gets the record (DTO) class
	 */
	public Class<? extends AbstractRecord> getRecordClass() {
		return recordClass;
	}
	
	/**
	 * Gets the criteria (DTO) class
	 */
	public Class<?> getCriteriaClass() {
		return criteriaClass;
	}
	
	/**
	 * Sets the record (DTO) class
	 */
	public final void setRecordClass(Class<? extends AbstractRecord> recordClass) {
		boolean isParameterCorrect = false;
		try {
			if (recordClass.newInstance() instanceof AbstractRecord)
			{
				isParameterCorrect = true;
			}
		} catch (Exception e) {
			// if there are any exception during instantiate an object, it will
			// throw an error as illegal argument exception
			throw new IllegalArgumentException(e);
		}

		// Throw illegal argument exception if recordClass is not instance of
		// AbstractRecord
		if (!isParameterCorrect)
			throw new IllegalArgumentException(
					"Class is not a subclass of AbstractRecord ");
		this.recordClass = recordClass;
		
		// Try to guess the Criteria class
		if (this.recordClass.getName().endsWith("Dto"));
		StringBuffer criteriaClassName = new StringBuffer(this.recordClass.getName());
		criteriaClassName.delete(criteriaClassName.length()-3, criteriaClassName.length());
		criteriaClassName.append("Criteria");
		try {
			this.criteriaClass = Class.forName(criteriaClassName.toString());
		} catch (ClassNotFoundException e) {
			;
		}
	}
	
	

	/**
	 * Factory function to crate a specific instance of record
	 */
	public AbstractRecord createRecordInstance() throws InstantiationException, IllegalAccessException
	{
		return (AbstractRecord)getRecordClass().newInstance();
	}
	
	public ICriteria createCriteriaInstance() 
	{
		try {
			return (ICriteria)getCriteriaClass().newInstance();
		} catch (Exception e)
		{
			throw new IllegalArgumentException(e);
		}
	}
	
	public ICriteria createCriteriaInstance(AbstractRecord record)  throws InstantiationException, IllegalAccessException
	{
		return (ICriteria)getCriteriaClass().newInstance();
	}

	/**
	 * Checks weather the criteria is sufficient to return unique result
	 * @param criteria
	 * @return true if sufficient to return unique result, false otherwise
	 */
	public boolean returnsUnique(ICriteria criteria) {
		if ( RecordUtils.isSidPositive(criteria.getSid()) ) {
			return true;
		}
		return false;
	}

	/**
	 * Pre-processes the record for insert.
	 * It sets the createDate field to current date.
	 * @param record the record to be inserted
	 */
	protected void prepareRecordForInsert(AbstractRecord record)
	{
		/* This logic was moved to AbstractDao, since settings are all primitive values */
		// There are some cases (Project) which uses UUID to set as default Id.
		record.setCreateDt(new Date());
		if (record.getUuid() == null || record.getUuid().length() < 2) {
			record.setUuid(generateUuid());
		}
	}
	
	/**
	 * Pre-processes the record for update.
	 * It sets the lastModifiedDate field to current date.
	 * 
	 * @param record the record to be updated
	 */
	protected void prepareRecordForUpdate(AbstractRecord record)
	{
		/*
		record.setLastModifiedDt(new Date());
		*/
	}

	/**
	 * Get a record from received criteria
	 * 
	 * @param criteria
	 * @return a record which properly match with the criteria
	 */
	public abstract AbstractRecord getRecord(RequestContext requestContext, ICriteria criteria)
			throws ServiceException;

	/**
	 * Get records from received criteria
	 * 
	 * @param criteria
	 * @return a list of records which properly match with the criteria
	 */
	public abstract List<AbstractRecord> listRecords(RequestContext requestContext, ICriteria criteria, QueryParams params)
			throws ServiceException;

	/**
	 * Insert a record to database
	 * 
	 * @param record
	 *            added record
	 * @return a sequence number of added record
	 */
	public abstract Long insertRecord(RequestContext requestContext, AbstractRecord record)
			throws ServiceException;

	/**
	 * Remove a record from database
	 * 
	 * @param record
	 *            removed record
	 * @return a number of affected record
	 */
	public abstract int deleteRecord(RequestContext requestContext, ICriteria criteria)
			throws ServiceException;

	/**
	 * Update a record in database
	 * 
	 * @param record
	 *            updated record
	 * @return a number of affected record
	 */
	public abstract int updateRecord(RequestContext requestContext, AbstractRecord record)
			throws ServiceException;
	

	/**
	 * Update a record in database
	 * 
	 * @param oldRecord
	 *            previous record state
	 * @param newRecord
	 *            updated record
	 * @return a number of affected record
	 */
	@Transactional
	public int updateRecord(RequestContext requestContext, AbstractRecord oldRecord, AbstractRecord newRecord)
			throws ServiceException
	{
		return updateRecord(requestContext, newRecord);
	}
	
	/**
	 * Generates a UUID
	 * @return
	 */
	public String generateUuid()
	{
		UUID uuid = UUID.randomUUID();
		return uuid.toString();
	}
	
	/**
	 * This method is mainly used by importer
	 * Before inserting, it 
	 */
	@Transactional
	public Long prepareReferencesAndInsert(RequestContext requestContext, AbstractRecord record) throws ServiceException
	{
		PropertyDescriptor[] propDescrs = PropertyUtils.getPropertyDescriptors(record);
		
		// Iterates through all properties 
		for (PropertyDescriptor propDescr: propDescrs) {
			
			// If the property is of type AbstractRecord, then retrieve from database
			Class<?> propType = propDescr.getPropertyType();
			try {
				if (propType != null) { 
					// TODO Handle AbstracRecord dto's
					if (AbstractRecord.class.isAssignableFrom(propDescr.getPropertyType()))
					{
						AbstractRecord dtoPropVal;
						try {
							dtoPropVal = (AbstractRecord)PropertyUtils.getProperty(record, propDescr.getName());
						} catch (InvocationTargetException ite) {
							if (ite.getTargetException() instanceof UnsupportedOperationException)
								continue; // Ignore, this operation is disabled
							else {
								throw ite;
							}
						}
						if (dtoPropVal != null) {
							// Process Inner DTO's

							//System.out.println("assigning:  "+record.getClass().getSimpleName()+"."+ propDescr.getName()+"="+dtoPropVal.toString());
							IDataOrientedService doSvc = ServiceContainer.getInstance().getDataOrientedService(propDescr.getPropertyType());
							if (doSvc == null)
								throw new IllegalStateException("Data Oriented Service for DTO "+ propDescr.getPropertyType().getSimpleName()+" not found");
							
							ICriteria criteria = doSvc.createCriteriaInstance(dtoPropVal);
							criteria.setSid(null); // sets the SID to null so the data oriented service finds it using other indexed field(s)
							AbstractRecord retrievedVal = null;
							try {
								retrievedVal = doSvc.getRecord(requestContext, criteria);
							} catch (ServiceException se){
								if ( !(se.getCause() != null && (se.getCause().getCause() instanceof IllegalArgumentException)) )
									throw se;
								// if this exception thrown, means that criteria is null
							}
							
							PropertyUtils.setProperty(record, propDescr.getName(), retrievedVal);
						}
						
					}
					// Handle Collection of AbstracRecords
					else if (List.class.isAssignableFrom( propDescr.getPropertyType()) ) {
						@SuppressWarnings("rawtypes")
						List propValCollection = null;
						try {
							propValCollection = (List)PropertyUtils.getProperty(record, propDescr.getName());
						} catch (InvocationTargetException ite) {
							if (ite.getTargetException() instanceof UnsupportedOperationException)
								continue; // Ignore, this operation is disabled
							else throw ite;
						}
						
						if (propValCollection != null) {
							for (int i=0;  i < propValCollection.size(); i++) {
								Object element = propValCollection.get(i);
								if (element != null && element instanceof AbstractRecord) {
									AbstractRecord elementRecord = (AbstractRecord)element;
									elementRecord.setSid(null); // sets the SID to null so the data oriented service finds it using other indexed field(s)
									IDataOrientedService doSvc = ServiceContainer.getInstance().getDataOrientedService(element.getClass());
									if (doSvc == null)
										throw new ServiceException("Data Oriented Service for "+element.getClass().getName()+" not found");
									
									ICriteria criteria = doSvc.createCriteriaInstance(elementRecord);
									AbstractRecord retrievedVal = doSvc.getRecord(requestContext, criteria);
									
									propValCollection.set(i, retrievedVal);
								}
							}
						}
						
					}
					
				}
			} catch (IllegalAccessException e) {
				throw new ServiceException(e);
			} catch (InvocationTargetException e) {
				throw new ServiceException(e);
			} catch (Exception e) {
				throw new ServiceException(e);
			}
		}
		
		ICriteria criteria;
		try {
			criteria = this.createCriteriaInstance(record);
		} catch (Exception e) {
			throw new ServiceException(e);
		}
		criteria.setSid(null);
		AbstractRecord matchRecord = this.getRecord(requestContext, criteria);
		if (matchRecord == null)
			return insertRecord(requestContext, record);
		else return 0l;
	}
	
}
